package ru.maxmatveev.beanmapper;

import javassist.*;

import java.lang.reflect.*;
import java.lang.reflect.Modifier;
import java.util.*;
import org.apache.log4j.Logger;

/**
 * @author Max Matveev
 *         Date: Jan 23, 2009
 *         Time: 10:10:13 PM
 */
public class MapperFactory {
    private static final Logger log = Logger.getLogger(MapperFactory.class);
    /**
     * Field uniqueIndex
     */
    private static long uniqueIndex = 0;
    /**
     * Field implementedMethods
     */
    private static Set<String> implementedMethods;
    /**
     * Field mapper
     */
    private static CtClass mapper;

    /**
     * Field sourceTypeParameters
     */
    private static Map<Type, Class> sourceTypeParameters;
    /**
     * Field destinationTypeParameters
     */
    private static Map<Type, Class> destinationTypeParameters;

    private static boolean clone;

    /**
     * Generate Mapper interface implementation.
     *
     * @param clazz Interface/Abstract class to implement. Only Abstract methods annotated with @Mapper are processed.
     * @return Object Mapper instance
     */

    public static Object getInstance(final Class<?> clazz) {
        uniqueIndex = 0;
        implementedMethods = new HashSet<String>();
        sourceTypeParameters = new HashMap<Type, Class>();
        destinationTypeParameters = new HashMap<Type, Class>();
        //long timer = System.nanoTime();
        if (clazz == null) {
            throw new IllegalArgumentException("Factory parameter cannot be null");
        }
        String implClassName = clazz.getName() + "BeanMapperImpl";
        String implClassShortName = clazz.getSimpleName() + "BeanMapperImpl";
        try {
            //DAO implementation class is already loaded
            Class mapperImpl = Class.forName(implClassName);
            return mapperImpl.newInstance();
        } catch (ClassNotFoundException ex) {
            //DAO implementation class is not loaded
            try {
                ClassPool classPool = ClassPool.getDefault();
                classPool.insertClassPath(new ClassClassPath(clazz));
                if (clazz.isInterface()) {
                    //DAO class passed is interface
                    mapper = classPool.makeClass(implClassName);
                    mapper.addInterface(classPool.get(clazz.getName()));
                } else {
                    //DAO class passed is [abstract] class
                    CtClass parent = classPool.get(clazz.getName());
                    mapper = classPool.makeClass(implClassName, parent);
                }
                mapper.addConstructor(CtNewConstructor.make(String.format("public %s() {}", implClassShortName), mapper));
                for (Method method : clazz.getMethods()) {
                    if (isMapper(method)) {
                        clone = method.getAnnotation(Mapper.class).forceClone();
                        String t = tabs(1);
                        final StringBuilder out = new StringBuilder();
                        String destinationObjectName = getUniqueName("destinationObject_");
                        final boolean cloneToNew = method.getParameterTypes().length == 1;
                        if (cloneToNew) {
                            out.append("\t/* Converting ").append(method.getParameterTypes()[0].getName()).append(" to ").append(method.getReturnType().getName()).append(" */\n");
                            out.append(String.format("\t%s %s %s(%s param) {\n", Modifier.toString(method.getModifiers() & ~Modifier.ABSTRACT), method.getReturnType().getName(), method.getName(), method.getParameterTypes()[0].getName()));
                            try {
                                if (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType())) {
                                    out.append(String.format("%s\t%s %s = new %s();\n", t, method.getReturnType().getName().replaceAll("\\$", "."), destinationObjectName, getImpl(method.getReturnType()).getConstructor().getName()));
                                } else if (Modifier.isPublic(method.getReturnType().getConstructor().getModifiers())) {
                                    out.append(String.format("%s\t%s %s = new %s();\n", t, method.getReturnType().getName().replaceAll("\\$", "."), destinationObjectName, method.getReturnType().getConstructor().getName()));
                                } else {
                                    throw new IllegalArgumentException("Cannot instantiate destiantion class");
                                }
                            } catch (NoSuchMethodException e) {
                                throw new IllegalArgumentException("Cannot instantiate destiantion class");
                            }
                        } else {
                            out.append("\t/* Converting ").append(method.getParameterTypes()[0].getName()).append(" to ").append(method.getParameterTypes()[1].getName()).append(" */\n");
                            out.append(String.format("\t%s void %s(%s param, %s param2) {\n", Modifier.toString(method.getModifiers() & ~Modifier.ABSTRACT), method.getName(), method.getParameterTypes()[0].getName(), method.getParameterTypes()[1].getName()));
                            out.append(String.format("%s\t%s %s = param2;\n", t, method.getParameterTypes()[1].getName().replaceAll("\\$", "."), destinationObjectName));
                        }
                        Type genericType = cloneToNew ? method.getGenericReturnType() : method.getGenericParameterTypes()[1];
                        Class<?> type = cloneToNew ? method.getReturnType() : method.getParameterTypes()[1];
                        if (genericType instanceof ParameterizedType) {
                            TypeVariable[] typerVars = ((Class) ((ParameterizedType) genericType).getRawType()).getTypeParameters();
                            for (int i = 0; i < typerVars.length; i++) {
                                destinationTypeParameters.put(typerVars[i], (Class) ((ParameterizedType) genericType).getActualTypeArguments()[i]);
                            }
                        }
                        if (method.getGenericParameterTypes()[0] instanceof ParameterizedType) {
                            TypeVariable[] typerVars = ((Class) ((ParameterizedType) method.getGenericParameterTypes()[0]).getRawType()).getTypeParameters();
                            for (int i = 0; i < typerVars.length; i++) {
                                sourceTypeParameters.put(typerVars[i], (Class) ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[i]);
                            }
                        }
                        if (cloneToNew) {
                            processObject(method.getParameterTypes()[0], type, method.getGenericParameterTypes()[0], genericType, "param", destinationObjectName, out, 2);
                            out.append(String.format("\t return %s;\n\t}\n", destinationObjectName));
                        } else {
                            processExistingObject(method.getParameterTypes()[0], type, method.getGenericParameterTypes()[0], genericType, "param", destinationObjectName, out, 2);
                            out.append(String.format("\t}\n"));
                        }
                        log.debug(out.toString());
//                        System.out.println(out.toString());
                        CtMethod implMethod = CtNewMethod.make(out.toString(), mapper);
                        mapper.addMethod(implMethod);
                    }
                }
                Class mapperImpl = mapper.toClass(String.class.getClassLoader(), null);
                mapper.detach();
                return mapperImpl.newInstance();
            } catch (NotFoundException e) {
                throw new IllegalArgumentException("Super interface/class for mapper is not found.", e);
            } catch (Exception e) {
                throw new IllegalStateException("Cannot create new mapper class.", e);
            }
        } catch (Exception ex) {
            throw new IllegalStateException("Cannot instantiate existing mapper class.", ex);
        }
    }

    /**
     * Check if method is Mapper method and should be implemented.
     *
     * @param method method to check
     * @return true if method is Mapper method and should be implemented
     */

    private static boolean isMapper(final Method method) {
        return method.getAnnotation(Mapper.class) != null && ((!method.getReturnType().getName().equals("void") && method.getParameterTypes().length == 1) || ((method.getReturnType().getName().equals("void") && method.getParameterTypes().length == 2))) &&
                Modifier.isAbstract(method.getModifiers());
    }

    /**
     * Generate tab characters.
     *
     * @param number number of tabs to generate
     * @return String
     */

    private static String tabs(int number) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < number; i++) {
            sb.append("\t");
        }
        return sb.toString();
    }

    /**
     * Generate unique name.
     *
     * @param prefix prefix
     * @return String name
     */

    private static String getUniqueName(String prefix) {
        return prefix + (uniqueIndex++);
    }

    /**
     * Generate object mapping.
     *
     * @param sourceClass           source class
     * @param destinationClass      destination class
     * @param sourceGeneric         source generic declaration if any, or null
     * @param destinationGeneric    destination generic declaration if any, or null
     * @param sourceObjectName      source object variable name
     * @param destinationObjectName destination object variable name
     * @param out                   output string builder
     * @param depth                 formatting depth
     * @throws CannotCompileException when source code generated cannot be compiled
     */
    public static void processObject(Class sourceClass, Class destinationClass, Type sourceGeneric, Type destinationGeneric, String sourceObjectName, String destinationObjectName, StringBuilder out, int depth) throws CannotCompileException {
        log.debug("process: " + out.toString());
        String t = tabs(depth);
        if (sourceClass.isEnum() || destinationClass.isEnum()) {
            out.append(String.format("%s/* Processing enum */\n", t));
            processEnum(destinationClass, sourceObjectName, destinationObjectName, out, depth);
        } else if (Collection.class.isAssignableFrom(sourceClass) && Collection.class.isAssignableFrom(destinationClass)) {
            out.append(String.format("%s/* Processing Collection */\n", t));
            if (sourceGeneric == null || destinationGeneric == null) {
                throw new IllegalArgumentException("Current version of mapper cannot be used to map Collection directly or when one of (sub)entity properties is not parametrized Collection");
            }
            processCollection((ParameterizedType) sourceGeneric, (ParameterizedType) destinationGeneric, sourceObjectName, destinationObjectName, out, depth);
        } else if (Map.class.isAssignableFrom(sourceClass) && Map.class.isAssignableFrom(destinationClass)) {
            out.append(String.format("%s/* Processing Map */\n", t));
            if (sourceGeneric == null || destinationGeneric == null) {
                throw new IllegalArgumentException("Current version of mapper cannot be used to map Map directly or when one of (sub)entity properties is not parametrized Map");
            }
            processMap((ParameterizedType) sourceGeneric, (ParameterizedType) destinationGeneric, sourceObjectName, destinationObjectName, out, depth);
        } else if (sourceClass.getName().startsWith("[") || destinationClass.getName().startsWith("[")) {
            out.append(String.format("%s/* Processing Array */\n", t));
            out.append(String.format("%s/* %s -> %s */\n", t, sourceClass.getName(), destinationClass.getName()));
            out.append(String.format("%s/* %s -> %s */\n", t, sourceGeneric.toString(), destinationGeneric.toString()));
            processArray(sourceGeneric, destinationGeneric, sourceObjectName, destinationObjectName, out, depth);
        } else if ((!clone || sourceClass.getName().startsWith("java.")) && destinationClass.isAssignableFrom(sourceClass) && (destinationGeneric == null || destinationGeneric.equals(sourceGeneric))) {
            out.append(String.format("%s/* Processint direct assingment */\n", t));
            out.append(String.format("%s%s = %s;\n", t, destinationObjectName, sourceObjectName));
        } else if ((Number.class.isAssignableFrom(sourceClass) || sourceClass.isPrimitive()) || (Number.class.isAssignableFrom(destinationClass) || destinationClass.isPrimitive())) {
            out.append(String.format("%s/* Processing primitive */\n", t));
            processPrimitive(sourceClass, destinationClass, sourceObjectName, destinationObjectName, out, depth);
        } else {
            out.append(String.format("%s/* Processing Bean */\n", t));
            String methodName = "map_" + sourceClass.getName().replaceAll("\\$", ".").replaceAll("\\.", "_") + "__" + (sourceGeneric == null ? "" : sourceGeneric.toString().replaceAll("[\\$\\. ><,]", "_")) + "_to_" + destinationClass.getName().replaceAll("\\$", ".").replaceAll("\\.", "_") + "__" + (destinationGeneric == null ? "" : destinationGeneric.toString().replaceAll("[\\$\\. ><,]", "_"));
            out.append(String.format("%s%s = %s(%s);\n", t, destinationObjectName, methodName, sourceObjectName));
            if (!implementedMethods.contains(methodName)) {
                log.debug("Implementing " + methodName);
                implementedMethods.add(methodName);
                sourceObjectName = "source";
                StringBuilder impl = new StringBuilder();
                impl.append(String.format("%spublic %s %s(%s source) {\n", t, destinationClass.getName().replaceAll("\\$", "."), methodName, sourceClass.getName().replaceAll("\\$", ".")));
                impl.append(String.format("%s\tif (source == null) {return null;}\n", t));
                destinationObjectName = getUniqueName("destinationObject_");
                try {
                    if (Collection.class.isAssignableFrom(destinationClass) || Map.class.isAssignableFrom(destinationClass)) {
                        impl.append(String.format("%s\t%s %s = new %s();\n", t, destinationClass.getName().replaceAll("\\$", "."), destinationObjectName, getImpl(destinationClass).getConstructor().getName()));
                    } else if (Modifier.isPublic(destinationClass.getConstructor().getModifiers())) {
                        impl.append(String.format("%s\t%s %s = new %s();\n", t, destinationClass.getName().replaceAll("\\$", "."), destinationObjectName, destinationClass.getConstructor().getName()));
                    } else {
                        throw new IllegalArgumentException("Cannot instantiate destiantion class " + destinationClass.getName());
                    }
                } catch (NoSuchMethodException e) {
                    throw new IllegalArgumentException("Cannot instantiate destiantion class" + destinationClass.getName());
                }
                for (Method setter : destinationClass.getMethods()) {
                    if (!isSetter(setter)) {
                        continue;
                    }
                    Method getter = getGetter(sourceClass, setter);
                    if (getter == null) {
                        impl.append(String.format("%s/* Skipping %s as not getter for it found in source class */\n", t, setter.getName()));
                        continue;
                    }
                    String sourcePropertyName = getUniqueName("sourceProperty_");
                    String destinationPropertyName = getUniqueName("destinationProperty_");
                    if (!getter.getReturnType().getName().startsWith("[")) {
                        impl.append(String.format("%s%s %s = %s.%s();\n", t, getter.getReturnType().getName().replaceAll("\\$", "."), sourcePropertyName, sourceObjectName, getter.getName()));
                    } else {
                        impl.append(String.format("%s/* Create array var definition */\n", t));
                        String sourceType = getter.getReturnType().getName();
                        String sourceTypeSuffix = "";
                        while (sourceType.startsWith("[")) {
                            sourceType = sourceType.substring(1);
                            sourceTypeSuffix += "[]";
                        }
                        if (sourceType.length() == 1) {
                            sourceType = getPrimitiveName(sourceType);
                        } else {
                            sourceType = sourceType.substring(1, sourceType.length() - 1).replaceAll("\\$", ".");
                        }
                        sourceType += sourceTypeSuffix;
                        impl.append(String.format("%s/* Created array var definition: %s */\n", t, sourceType));
                        impl.append(String.format("%s%s %s = %s.%s();\n", t, sourceType, sourcePropertyName, sourceObjectName, getter.getName()));
                    }
                    try {
                        if (setter.getParameterTypes()[0].getName().startsWith("[")) {
                            impl.append(String.format("%s/* Create array var definition */\n", t));
                            String lengthVarName = getUniqueName("arrayLength_");
                            impl.append(String.format("%sint %s;\n", t, lengthVarName));
                            String lengthGetter;
                            if (Collection.class.isAssignableFrom(getter.getReturnType())) {
                                lengthGetter = "size()";
                            } else if (getter.getReturnType().getName().startsWith("[")) {
                                lengthGetter = "length";
                            } else {
                                throw new IllegalArgumentException("Source type for array type can be only array or Collection type");
                            }
                            impl.append(String.format("%sif (%s == null) {\n%s\t%s = 0;\n%s} else {\n%s\t%s = %s.%s;\n%s}\n", t, sourcePropertyName, t, lengthVarName, t, t, lengthVarName, sourcePropertyName, lengthGetter, t));
                            String destinationType = setter.getParameterTypes()[0].getName();
                            String destinationTypeSuffix = "";
                            while (destinationType.startsWith("[")) {
                                destinationType = destinationType.substring(1);
                                if (destinationTypeSuffix.length() == 0) {
                                    destinationTypeSuffix += "[%s]";
                                } else {
                                    destinationTypeSuffix += "[]";
                                }
                            }
                            if (destinationType.length() == 1) {
                                destinationType = getPrimitiveName(destinationType);
                            } else {
                                destinationType = destinationType.substring(1, destinationType.length() - 1).replaceAll("\\$", ".");
                            }
                            destinationType += destinationTypeSuffix;
                            impl.append(String.format("%s/* Created array var definition: %s */\n", t, destinationType));
                            impl.append(String.format("%s%s %s = new %s;\n", t, String.format(destinationType, ""), destinationPropertyName, String.format(destinationType, lengthVarName)));
                        } else if (Collection.class.isAssignableFrom(setter.getParameterTypes()[0]) || Map.class.isAssignableFrom(setter.getParameterTypes()[0])) {
                            impl.append(String.format("%s%s %s = new %s();\n", t, setter.getParameterTypes()[0].getName().replaceAll("\\$", "."), destinationPropertyName, getImpl(setter.getParameterTypes()[0]).getConstructor().getName()));
                        } else if (Modifier.isPublic(setter.getParameterTypes()[0].getConstructor().getModifiers())) {
                            impl.append(String.format("%s%s %s = new %s();\n", t, setter.getParameterTypes()[0].getName().replaceAll("\\$", "."), destinationPropertyName, setter.getParameterTypes()[0].getConstructor().getName()));
                        } else {
                            impl.append(String.format("%s%s %s = %s.%s();\n", t, setter.getParameterTypes()[0].getName().replaceAll("\\$", "."), destinationPropertyName, destinationObjectName, getGetter(destinationClass, setter).getName()));
                        }
                    } catch (NoSuchMethodException e) {
                        impl.append(String.format("%s%s %s = %s.%s();\n", t, setter.getParameterTypes()[0].getName().replaceAll("\\$", "."), destinationPropertyName, destinationObjectName, getGetter(destinationClass, setter).getName()));
                    }
                    processObject(getter.getReturnType(), setter.getParameterTypes()[0], getter.getGenericReturnType(), setter.getGenericParameterTypes()[0], sourcePropertyName, destinationPropertyName, impl, depth);
                    impl.append(String.format("%s%s.%s(%s);\n\n", t, destinationObjectName, setter.getName(), destinationPropertyName));
                }
                impl.append(String.format("%sreturn %s;\n}\n", t, destinationObjectName));
                log.debug(impl.toString());
//                System.out.println(impl.toString());
                CtMethod implMethod = CtNewMethod.make(impl.toString(), mapper);
                mapper.addMethod(implMethod);
            } else {
                log.debug(methodName + " already implemented");
            }
        }
    }

    /**
     * Generate object mapping.
     *
     * @param sourceClass           source class
     * @param destinationClass      destination class
     * @param sourceGeneric         source generic declaration if any, or null
     * @param destinationGeneric    destination generic declaration if any, or null
     * @param sourceObjectName      source object variable name
     * @param destinationObjectName destination object variable name
     * @param out                   output string builder
     * @param depth                 formatting depth
     * @throws CannotCompileException when source code generated cannot be compiled
     */
    public static void processExistingObject(Class sourceClass, Class destinationClass, Type sourceGeneric, Type destinationGeneric, String sourceObjectName, String destinationObjectName, StringBuilder out, int depth) throws CannotCompileException {
        String t = tabs(depth);
        if (sourceClass.isEnum() || destinationClass.isEnum()) {
            out.append(String.format("%s/* Processing enum */\n", t));
            processEnum(destinationClass, sourceObjectName, destinationObjectName, out, depth);
        } else if (Collection.class.isAssignableFrom(sourceClass) && Collection.class.isAssignableFrom(destinationClass)) {
            out.append(String.format("%s/* Processing Collection */\n", t));
            if (sourceGeneric == null || destinationGeneric == null) {
                throw new IllegalArgumentException("Current version of mapper cannot be used to map Collection directly or when one of (sub)entity properties is not parametrized Collection");
            }
//            processCollection((ParameterizedType) sourceGeneric, (ParameterizedType) destinationGeneric, sourceObjectName, destinationObjectName, out, depth);
        } else if (Map.class.isAssignableFrom(sourceClass) && Map.class.isAssignableFrom(destinationClass)) {
            out.append(String.format("%s/* Processing Map */\n", t));
            if (sourceGeneric == null || destinationGeneric == null) {
                throw new IllegalArgumentException("Current version of mapper cannot be used to map Map directly or when one of (sub)entity properties is not parametrized Map");
            }
//            processMap((ParameterizedType) sourceGeneric, (ParameterizedType) destinationGeneric, sourceObjectName, destinationObjectName, out, depth);
        } else if (sourceClass.getName().startsWith("[") || destinationClass.getName().startsWith("[")) {
            out.append(String.format("%s/* Processing Array */\n", t));
            out.append(String.format("%s/* %s -> %s */\n", t, sourceClass.getName(), destinationClass.getName()));
            out.append(String.format("%s/* %s -> %s */\n", t, sourceGeneric.toString(), destinationGeneric.toString()));
//            processArray(sourceGeneric, destinationGeneric, sourceObjectName, destinationObjectName, out, depth);
        } else if (destinationClass.isAssignableFrom(sourceClass) && (destinationGeneric == null || destinationGeneric.equals(sourceGeneric))) {
            out.append(String.format("%s/* Processint direct assingment */\n", t));
            out.append(String.format("%s%s = %s;\n", t, destinationObjectName, sourceObjectName));
        } else if ((Number.class.isAssignableFrom(sourceClass) || sourceClass.isPrimitive()) || (Number.class.isAssignableFrom(destinationClass) || destinationClass.isPrimitive())) {
            out.append(String.format("%s/* Processing primitive */\n", t));
            processPrimitive(sourceClass, destinationClass, sourceObjectName, destinationObjectName, out, depth);
        } else {
            out.append(String.format("%s/* Processing Bean */\n", t));
            String methodName = "map_existing_" + sourceClass.getName().replaceAll("\\$", ".").replaceAll("\\.", "_") + "__" + (sourceGeneric == null ? "" : sourceGeneric.toString().replaceAll("[\\$\\. ><,]", "_")) + "_to_" + destinationClass.getName().replaceAll("\\$", ".").replaceAll("\\.", "_") + "__" + (destinationGeneric == null ? "" : destinationGeneric.toString().replaceAll("[\\$\\. ><,]", "_"));
            out.append(String.format("%s%s(%s, %s);\n", t, methodName, sourceObjectName, destinationObjectName));
            if (!implementedMethods.contains(methodName)) {
                implementedMethods.add(methodName);
                sourceObjectName = "source";
                StringBuilder impl = new StringBuilder();
                destinationObjectName = getUniqueName("destinationObject_");
                impl.append(String.format("%spublic %s %s(%s source, %s %s) {\n", t, destinationClass.getName().replaceAll("\\$", "."), methodName, sourceClass.getName().replaceAll("\\$", "."), destinationClass.getName().replaceAll("\\$", "."), destinationObjectName));
                try {
                    if (Collection.class.isAssignableFrom(destinationClass) || Map.class.isAssignableFrom(destinationClass)) {
                        impl.append(String.format("%s\tif (%s == null) {\n", t, destinationObjectName));
                        impl.append(String.format("%s\t\t%s = new %s();\n", t, destinationObjectName, getImpl(destinationClass).getConstructor().getName()));
                        impl.append(String.format("%s\t}\n", t));
                    } else if (Modifier.isPublic(destinationClass.getConstructor().getModifiers())) {
                        impl.append(String.format("%s\tif (%s == null) {\n", t, destinationObjectName));
                        impl.append(String.format("%s\t%s = new %s();\n", t, destinationObjectName, destinationClass.getConstructor().getName()));
                        impl.append(String.format("%s\t}\n", t));
                    } else {
                        throw new IllegalArgumentException("Cannot instantiate destiantion class " + destinationClass.getName());
                    }
                } catch (NoSuchMethodException e) {
                    throw new IllegalArgumentException("Cannot instantiate destiantion class" + destinationClass.getName());
                }
                for (Method setter : destinationClass.getMethods()) {
                    if (!isSetter(setter)) {
                        continue;
                    }
                    Method getter = getGetter(sourceClass, setter);
                    if (getter == null) {
                        impl.append(String.format("%s/* Skipping %s as not getter for it found in source class */\n", t, setter.getName()));
                        continue;
                    }
                    String sourcePropertyName = getUniqueName("sourceProperty_");
                    String destinationPropertyName = getUniqueName("destinationProperty_");
                    if (!getter.getReturnType().getName().startsWith("[")) {
                        impl.append(String.format("%s%s %s = %s.%s();\n", t, getter.getReturnType().getName().replaceAll("\\$", "."), sourcePropertyName, sourceObjectName, getter.getName()));
                    } else {
                        impl.append(String.format("%s/* Create array var definition */\n", t));
                        String sourceType = getter.getReturnType().getName();
                        String sourceTypeSuffix = "";
                        while (sourceType.startsWith("[")) {
                            sourceType = sourceType.substring(1);
                            sourceTypeSuffix += "[]";
                        }
                        if (sourceType.length() == 1) {
                            sourceType = getPrimitiveName(sourceType);
                        } else {
                            sourceType = sourceType.substring(1, sourceType.length() - 1).replaceAll("\\$", ".");
                        }
                        sourceType += sourceTypeSuffix;
                        impl.append(String.format("%s/* Created array var definition: %s */\n", t, sourceType));
                        impl.append(String.format("%s%s %s = %s.%s();\n", t, sourceType, sourcePropertyName, sourceObjectName, getter.getName()));
                    }
                    try {
                        if (setter.getParameterTypes()[0].getName().startsWith("[")) {
                            impl.append(String.format("%s/* Create array var definition */\n", t));
                            String lengthVarName = getUniqueName("arrayLength_");
                            impl.append(String.format("%sint %s;\n", t, lengthVarName));
                            String lengthGetter;
                            if (Collection.class.isAssignableFrom(getter.getReturnType())) {
                                lengthGetter = "size()";
                            } else if (getter.getReturnType().getName().startsWith("[")) {
                                lengthGetter = "length";
                            } else {
                                throw new IllegalArgumentException("Source type for array type can be only array or Collection type");
                            }
                            impl.append(String.format("%sif (%s == null) {\n%s\t%s = 0;\n%s} else {\n%s\t%s = %s.%s;\n%s}\n", t, sourcePropertyName, t, lengthVarName, t, t, lengthVarName, sourcePropertyName, lengthGetter, t));
                            String destinationType = setter.getParameterTypes()[0].getName();
                            String destinationTypeSuffix = "";
                            while (destinationType.startsWith("[")) {
                                destinationType = destinationType.substring(1);
                                if (destinationTypeSuffix.length() == 0) {
                                    destinationTypeSuffix += "[%s]";
                                } else {
                                    destinationTypeSuffix += "[]";
                                }
                            }
                            if (destinationType.length() == 1) {
                                destinationType = getPrimitiveName(destinationType);
                            } else {
                                destinationType = destinationType.substring(1, destinationType.length() - 1).replaceAll("\\$", ".");
                            }
                            destinationType += destinationTypeSuffix;
                            impl.append(String.format("%s/* Created array var definition: %s */\n", t, destinationType));
                            impl.append(String.format("%s%s %s = %s.%s();\n", t, String.format(destinationType, ""), destinationPropertyName, destinationObjectName, getGetter(destinationClass, setter).getName()));
                            impl.append(String.format("%sif(%s == null) {\n", t, destinationPropertyName));
                            impl.append(String.format("%s\t%s = new %s;\n", t, destinationPropertyName, String.format(destinationType, lengthVarName)));
                            impl.append(String.format("%s}\n", t));
                        } else if (Collection.class.isAssignableFrom(setter.getParameterTypes()[0]) || Map.class.isAssignableFrom(setter.getParameterTypes()[0])) {
                            impl.append(String.format("%s%s %s = %s.%s();\n", t, setter.getParameterTypes()[0].getName().replaceAll("\\$", "."), destinationPropertyName, destinationObjectName, getGetter(destinationClass, setter).getName()));
                            impl.append(String.format("%sif(%s == null) {\n", t, destinationPropertyName));
                            impl.append(String.format("%s\t%s = new %s();\n", t, destinationPropertyName, getImpl(setter.getParameterTypes()[0]).getConstructor().getName()));                            impl.append(String.format("%s}\n", t));

                        } else if (Modifier.isPublic(setter.getParameterTypes()[0].getConstructor().getModifiers())) {
                            impl.append(String.format("%s%s %s = %s.%s();\n", t, setter.getParameterTypes()[0].getName().replaceAll("\\$", "."), destinationPropertyName, destinationObjectName, getGetter(destinationClass, setter).getName()));
                            impl.append(String.format("%sif(%s == null) {\n", t, destinationPropertyName));
                            impl.append(String.format("%s\t%s = new %s();\n", t, destinationPropertyName, setter.getParameterTypes()[0].getConstructor().getName()));
                            impl.append(String.format("%s}\n", t));
                        } else {
                            impl.append(String.format("%s%s %s = %s.%s();\n", t, setter.getParameterTypes()[0].getName().replaceAll("\\$", "."), destinationPropertyName, destinationObjectName, getGetter(destinationClass, setter).getName()));
                        }
                    } catch (NoSuchMethodException e) {
                        impl.append(String.format("%s%s %s = %s.%s();\n", t, setter.getParameterTypes()[0].getName().replaceAll("\\$", "."), destinationPropertyName, destinationObjectName, getGetter(destinationClass, setter).getName()));
                    }
                    processExistingObject(getter.getReturnType(), setter.getParameterTypes()[0], getter.getGenericReturnType(), setter.getGenericParameterTypes()[0], sourcePropertyName, destinationPropertyName, impl, depth);
                    impl.append(String.format("%s%s.%s(%s);\n\n", t, destinationObjectName, setter.getName(), destinationPropertyName));
                }
                impl.append(String.format("%sreturn %s;\n}\n", t, destinationObjectName));
                log.debug(impl.toString());
//                System.out.println(impl.toString());
                CtMethod implMethod = CtNewMethod.make(impl.toString(), mapper);
                mapper.addMethod(implMethod);
            }
        }
    }

    /**
     * Generate enum mapping.
     *
     * @param destinationClass      destination class
     * @param sourceObjectName      source object variable name
     * @param destinationObjectName destination object variable name
     * @param out                   output string builder
     * @param depth                 formatting depth
     */

    private static void processEnum(final Class destinationClass, final String sourceObjectName, final String destinationObjectName, final StringBuilder out, final int depth) {
        String t = tabs(depth);
        out.append(String.format("%sif (%s == null) {\n%s\t%s = null;\n%s} else {\n", t, sourceObjectName, t, destinationObjectName, t));
        if (destinationClass.isEnum()) {
            out.append(String.format("%s\t%s = %s.valueOf(%s.toString());\n", t, destinationObjectName, destinationClass.getName().replaceAll("\\$", "."), sourceObjectName));
        } else {
            out.append(String.format("%s\t%s = %s.toString();\n", t, destinationObjectName, sourceObjectName));
        }
        out.append(String.format("%s}\n", t));
    }

    /**
     * Generate Collection mapping.
     *
     * @param sourceGeneric         source generic declaration
     * @param destinationGeneric    destination generic declaration
     * @param sourceObjectName      source object variable name
     * @param destinationObjectName destination object variable name
     * @param out                   output string builder
     * @param depth                 formatting depth
     * @throws CannotCompileException when source code generated cannot be compiled
     */

    private static void processCollection(final ParameterizedType sourceGeneric, final ParameterizedType destinationGeneric, final String sourceObjectName, final String destinationObjectName, final StringBuilder out, final int depth) throws CannotCompileException {
        String t = tabs(depth);
        out.append(String.format("%sif (%s == null) {\n%s\t%s = null;\n%s} else {\n", t, sourceObjectName, t, destinationObjectName, t));
        String iteratorName = getUniqueName("iterator_");
        out.append(String.format("%s\tfor (java.util.Iterator %s = %s.iterator(); %s.hasNext();) {\n", t, iteratorName, sourceObjectName, iteratorName));

        String sourcePropertyName = getUniqueName("sourceCollectionElement_");
        String destinationPropertyName = getUniqueName("destinationCollectionElement_");
        Class sourceElementClass;
        if (sourceGeneric.getActualTypeArguments()[0] instanceof Class) {
            sourceElementClass = (Class) sourceGeneric.getActualTypeArguments()[0];
        } else if (sourceGeneric.getActualTypeArguments()[0] instanceof TypeVariable) {
            sourceElementClass = sourceTypeParameters.get(sourceGeneric.getActualTypeArguments()[0]);
        } else {
            sourceElementClass = (Class) ((ParameterizedType) sourceGeneric.getActualTypeArguments()[0]).getRawType();
        }
        Class destinationElementClass;
        if (destinationGeneric.getActualTypeArguments()[0] instanceof Class) {
            destinationElementClass = (Class) destinationGeneric.getActualTypeArguments()[0];
        } else if (destinationGeneric.getActualTypeArguments()[0] instanceof TypeVariable) {
            destinationElementClass = destinationTypeParameters.get(destinationGeneric.getActualTypeArguments()[0]);
        } else {
            destinationElementClass = (Class) ((ParameterizedType) destinationGeneric.getActualTypeArguments()[0]).getRawType();
        }
        out.append(String.format("%s\t\t%s %s = (%s) %s.next();\n", t, sourceElementClass.getName().replaceAll("\\$", "."), sourcePropertyName, sourceElementClass.getName().replaceAll("\\$", "."), iteratorName));
        try {
            if (Collection.class.isAssignableFrom(destinationElementClass) || Map.class.isAssignableFrom(destinationElementClass)) {
                out.append(String.format("%s\t\t%s %s = new %s();\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName, getImpl(destinationElementClass).getConstructor().getName()));
            } else if (Modifier.isPublic(destinationElementClass.getConstructor().getModifiers())) {
                out.append(String.format("%s\t\t%s %s = new %s();\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName, destinationElementClass.getConstructor().getName()));
            } else {
                out.append(String.format("%s\t\t%s %s = null;\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName));
            }
        } catch (NoSuchMethodException e) {
            out.append(String.format("%s\t\t%s %s = null;\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName));
        }
        processObject(sourceElementClass, destinationElementClass, sourceGeneric.getActualTypeArguments()[0], destinationGeneric.getActualTypeArguments()[0], sourcePropertyName, destinationPropertyName, out, depth + 2);
        out.append(String.format("%s\t\t%s.add(%s);\n", t, destinationObjectName, destinationPropertyName));
        out.append(String.format("%s\t}\n%s}\n\n", t, t));
    }

    /**
     * Generate Map mapping.
     *
     * @param sourceGeneric         source generic declaration
     * @param destinationGeneric    destination generic declaration
     * @param sourceObjectName      source object variable name
     * @param destinationObjectName destination object variable name
     * @param out                   output string builder
     * @param depth                 formatting depth
     * @throws CannotCompileException when source code generated cannot be compiled
     */

    private static void processMap(final ParameterizedType sourceGeneric, final ParameterizedType destinationGeneric, final String sourceObjectName, final String destinationObjectName, final StringBuilder out, final int depth) throws CannotCompileException {
        String t = tabs(depth);
        out.append(String.format("%sif (%s == null) {\n%s\t%s = null;\n%s} else {\n", t, sourceObjectName, t, destinationObjectName, t));
        String iteratorName = getUniqueName("iterator_");
        out.append(String.format("%s\tfor (java.util.Iterator %s = %s.keySet().iterator(); %s.hasNext();) {\n", t, iteratorName, sourceObjectName, iteratorName));

        String sourcePropertyName = getUniqueName("sourceMapElement_");
        String destinationPropertyName = getUniqueName("destinationMapElement_");
        String sourceKeyName = getUniqueName("sourceKeyElement_");
        String destinationKeyName = getUniqueName("destinationKeyElement_");

        Class sourceElementClass;
        if (sourceGeneric.getActualTypeArguments()[1] instanceof Class) {
            sourceElementClass = (Class) sourceGeneric.getActualTypeArguments()[1];
        } else if (sourceGeneric.getActualTypeArguments()[1] instanceof TypeVariable) {
            sourceElementClass = sourceTypeParameters.get(sourceGeneric.getActualTypeArguments()[1]);
        } else {
            sourceElementClass = (Class) ((ParameterizedType) sourceGeneric.getActualTypeArguments()[1]).getRawType();
        }
        Class destinationElementClass;
        if (destinationGeneric.getActualTypeArguments()[1] instanceof Class) {
            destinationElementClass = (Class) destinationGeneric.getActualTypeArguments()[1];
        } else if (destinationGeneric.getActualTypeArguments()[1] instanceof TypeVariable) {
            destinationElementClass = destinationTypeParameters.get(destinationGeneric.getActualTypeArguments()[1]);
        } else {
            destinationElementClass = (Class) ((ParameterizedType) destinationGeneric.getActualTypeArguments()[1]).getRawType();
        }
        Class sourceKeyClass;
        if (sourceGeneric.getActualTypeArguments()[0] instanceof Class) {
            sourceKeyClass = (Class) sourceGeneric.getActualTypeArguments()[0];
        } else if (sourceGeneric.getActualTypeArguments()[0] instanceof TypeVariable) {
            sourceKeyClass = sourceTypeParameters.get(sourceGeneric.getActualTypeArguments()[0]);
        } else {
            sourceKeyClass = (Class) ((ParameterizedType) sourceGeneric.getActualTypeArguments()[0]).getRawType();
        }
        Class destinationKeyClass;
        if (destinationGeneric.getActualTypeArguments()[0] instanceof Class) {
            destinationKeyClass = (Class) destinationGeneric.getActualTypeArguments()[0];
        } else if (destinationGeneric.getActualTypeArguments()[0] instanceof TypeVariable) {
            destinationKeyClass = destinationTypeParameters.get(destinationGeneric.getActualTypeArguments()[0]);
        } else {
            destinationKeyClass = (Class) ((ParameterizedType) destinationGeneric.getActualTypeArguments()[0]).getRawType();
        }

        out.append(String.format("%s\t\t%s %s = (%s) %s.next();\n", t, sourceKeyClass.getName().replaceAll("\\$", "."), sourceKeyName, sourceKeyClass.getName().replaceAll("\\$", "."), iteratorName));
        out.append(String.format("%s\t\t%s %s = (%s) %s.get(%s);\n", t, sourceElementClass.getName().replaceAll("\\$", "."), sourcePropertyName, sourceElementClass.getName().replaceAll("\\$", "."), sourceObjectName, sourceKeyName));
        try {
            if (Collection.class.isAssignableFrom(destinationElementClass) || Map.class.isAssignableFrom(destinationElementClass)) {
                out.append(String.format("%s\t\t%s %s = new %s();\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName, getImpl(destinationElementClass).getConstructor().getName()));
            } else if (Modifier.isPublic(destinationElementClass.getConstructor().getModifiers())) {
                out.append(String.format("%s\t\t%s %s = new %s();\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName, destinationElementClass.getConstructor().getName()));
            } else {
                out.append(String.format("%s\t\t%s %s = null;\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName));
            }
        } catch (NoSuchMethodException e) {
            out.append(String.format("%s\t\t%s %s = null;\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName));
        }
        try {
            if (Collection.class.isAssignableFrom(destinationKeyClass) || Map.class.isAssignableFrom(destinationKeyClass)) {
                out.append(String.format("%s\t\t%s %s = new %s();\n", t, destinationKeyClass.getName().replaceAll("\\$", "."), destinationKeyName, getImpl(destinationKeyClass).getConstructor().getName()));
            } else if (Modifier.isPublic(destinationKeyClass.getConstructor().getModifiers())) {
                out.append(String.format("%s\t\t%s %s = new %s();\n", t, destinationKeyClass.getName().replaceAll("\\$", "."), destinationKeyName, destinationKeyClass.getConstructor().getName()));
            } else {
                out.append(String.format("%s\t\t%s %s = null;\n", t, destinationKeyClass.getName().replaceAll("\\$", "."), destinationKeyName));
            }
        } catch (NoSuchMethodException e) {
            out.append(String.format("%s\t\t%s %s = null;\n", t, destinationKeyClass.getName().replaceAll("\\$", "."), destinationKeyName));
        }
        processObject(sourceElementClass, destinationElementClass, sourceGeneric.getActualTypeArguments()[1], destinationGeneric.getActualTypeArguments()[1], sourcePropertyName, destinationPropertyName, out, depth + 2);
        processObject(sourceKeyClass, destinationKeyClass, sourceGeneric.getActualTypeArguments()[0], destinationGeneric.getActualTypeArguments()[0], sourceKeyName, destinationKeyName, out, depth + 2);
        out.append(String.format("%s\t\t%s.put(%s, %s);\n", t, destinationObjectName, destinationKeyName, destinationPropertyName));
        out.append(String.format("%s\t}\n%s}\n\n", t, t));
    }

    /**
     * Generate array mapping.
     *
     * @param sourceGeneric         source generic declaration
     * @param destinationGeneric    destination generic declaration
     * @param sourceObjectName      source object variable name
     * @param destinationObjectName destination object variable name
     * @param out                   output string builder
     * @param depth                 formatting depth
     * @throws CannotCompileException when source code generated cannot be compiled
     */

    private static void processArray(Type sourceGeneric, Type destinationGeneric, String sourceObjectName, String destinationObjectName, StringBuilder out, int depth) throws CannotCompileException {
        String t = tabs(depth);
        out.append(String.format("%sif (%s == null) {\n%s\t%s = null;\n%s} else {\n", t, sourceObjectName, t, destinationObjectName, t));
        if (sourceGeneric instanceof ParameterizedType) {
            out.append(String.format("%s/* source is a Collection */\n", t));
            String indexVarName = getUniqueName("index_");
            out.append(String.format("%s\t\tint %s = 0;\n", t, indexVarName));

            String iteratorName = getUniqueName("iterator_");
            out.append(String.format("%s\tfor (java.util.Iterator %s = %s.iterator(); %s.hasNext();) {\n", t, iteratorName, sourceObjectName, iteratorName));

            String sourcePropertyName = getUniqueName("sourceCollectionElement_");
            String destinationPropertyName = getUniqueName("destinationCollectionElement_");
            Class sourceElementClass;
            if (((ParameterizedType) sourceGeneric).getActualTypeArguments()[0] instanceof Class) {
                sourceElementClass = (Class) ((ParameterizedType) sourceGeneric).getActualTypeArguments()[0];
            } else {
                sourceElementClass = (Class) ((ParameterizedType) ((ParameterizedType) sourceGeneric).getActualTypeArguments()[0]).getRawType();
            }
            Class destinationElementClass;
            try {
                destinationElementClass = Class.forName(((Class) destinationGeneric).getName().substring(1));
            } catch (ClassNotFoundException e) {
                try {
                    destinationElementClass = Class.forName(((Class) destinationGeneric).getName().substring(1, ((Class) destinationGeneric).getName().length() - 1));
                } catch (Exception e1) {
                    destinationElementClass = getBoxClass(((Class) destinationGeneric).getName().substring(1));
                }
            }

            String destinationElementClassName = ((Class) destinationGeneric).getName().substring(1);
            String destinationTypeSuffix = "";
            while (destinationElementClassName.startsWith("[")) {
                destinationElementClassName = destinationElementClassName.substring(1);
                if (destinationTypeSuffix.length() == 0) {
                    destinationTypeSuffix += "[%s]";
                } else {
                    destinationTypeSuffix += "[]";
                }
            }
            if (destinationElementClassName.length() == 1) {
                destinationElementClassName = getPrimitiveName(destinationElementClassName);
            } else {
                destinationElementClassName = destinationElementClassName.substring(1, destinationElementClassName.length() - 1).replaceAll("\\$", ".");
            }
            destinationElementClassName += destinationTypeSuffix;
            out.append(String.format("%s\t\t%s %s = (%s) %s.next();\n", t, sourceElementClass.getName().replaceAll("\\$", "."), sourcePropertyName, sourceElementClass.getName().replaceAll("\\$", "."), iteratorName));
            if (destinationElementClass.isArray()) {
                String lengthVarName = getUniqueName("arrayLength_");
                out.append(String.format("%s\t\tint %s;\n%s\t\tif (%s == null) {\n%s\t\t\t%s = 0;\n%s\t\t} else {\n%s\t\t\t%s = %s.size();\n%s\t\t}\n", t, lengthVarName, t, sourcePropertyName, t, lengthVarName, t, t, lengthVarName, sourcePropertyName, t));
                out.append(String.format("%s\t\t%s %s = new %s;\n", t, String.format(destinationElementClassName, ""), destinationPropertyName, String.format(destinationElementClassName, lengthVarName)));
            } else if (destinationElementClass.isPrimitive()) {
                out.append(String.format("%s\t\t%s %s = %s;\n", t, destinationElementClass.getName(), destinationPropertyName, getDefaultPrimitiveValue(destinationElementClass.getName())));
            } else {
                try {
                    if (Collection.class.isAssignableFrom(destinationElementClass) || Map.class.isAssignableFrom(destinationElementClass)) {
                        out.append(String.format("%s\t\t%s %s = new %s();\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName, getImpl(destinationElementClass).getConstructor().getName()));
                    } else if (Modifier.isPublic(destinationElementClass.getConstructor().getModifiers())) {
                        out.append(String.format("%s\t\t%s %s = new %s();\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName, destinationElementClass.getConstructor().getName()));
                    } else {
                        out.append(String.format("%s\t\t%s %s = null;\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName));
                    }
                } catch (NoSuchMethodException e) {
                    out.append(String.format("%s\t\t%s %s = null;\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName));
                }
            }
            processObject(sourceElementClass, destinationElementClass, ((ParameterizedType) sourceGeneric).getActualTypeArguments()[0], destinationElementClass, sourcePropertyName, destinationPropertyName, out, depth + 2);
            out.append(String.format("%s\t\t%s[%s++] = %s;\n", t, destinationObjectName, indexVarName, destinationPropertyName));
            out.append(String.format("%s\t}\n", t));
        } else if (destinationGeneric instanceof ParameterizedType) {
            out.append(String.format("%s/* destination is a Collection */\n", t));
            String indexVarName = getUniqueName("index_");
            out.append(String.format("%s\tfor (int %s = 0; %s < %s.length; %s++) {\n", t, indexVarName, indexVarName, sourceObjectName, indexVarName));
            String sourcePropertyName = getUniqueName("sourceCollectionElement_");
            String destinationPropertyName = getUniqueName("destinationCollectionElement_");
            Class destinationElementClass;
            if (((ParameterizedType) destinationGeneric).getActualTypeArguments()[0] instanceof Class) {
                destinationElementClass = (Class) ((ParameterizedType) destinationGeneric).getActualTypeArguments()[0];
            } else {
                destinationElementClass = (Class) ((ParameterizedType) ((ParameterizedType) destinationGeneric).getActualTypeArguments()[0]).getRawType();
            }
            Class sourceElementClass;
            try {
                sourceElementClass = Class.forName(((Class) sourceGeneric).getName().substring(1));
            } catch (ClassNotFoundException e) {
                try {
                    sourceElementClass = Class.forName(((Class) sourceGeneric).getName().substring(1, ((Class) sourceGeneric).getName().length() - 1));
                } catch (Exception e1) {
                    sourceElementClass = getBoxClass(((Class) sourceGeneric).getName().substring(1));
                }
            }
            String sourceElementClassName = ((Class) sourceGeneric).getName().substring(1);
            String sourceTypeSuffix = "";
            while (sourceElementClassName.startsWith("[")) {
                sourceElementClassName = sourceElementClassName.substring(1);
                if (sourceTypeSuffix.length() == 0) {
                    sourceTypeSuffix += "[%s]";
                } else {
                    sourceTypeSuffix += "[]";
                }
            }
            if (sourceElementClassName.length() == 1) {
                sourceElementClassName = getPrimitiveName(sourceElementClassName);
            } else {
                sourceElementClassName = sourceElementClassName.substring(1, sourceElementClassName.length() - 1).replaceAll("\\$", ".");
            }
            sourceElementClassName += sourceTypeSuffix;

            out.append(String.format("%s\t\t%s %s = %s[%s];\n", t, sourceElementClassName, sourcePropertyName, sourceObjectName, indexVarName));
            try {
                if (Collection.class.isAssignableFrom(destinationElementClass) || Map.class.isAssignableFrom(destinationElementClass)) {
                    out.append(String.format("%s\t\t%s %s = new %s();\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName, getImpl(destinationElementClass).getConstructor().getName()));
                } else if (Modifier.isPublic(destinationElementClass.getConstructor().getModifiers())) {
                    out.append(String.format("%s\t\t%s %s = new %s();\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName, destinationElementClass.getConstructor().getName()));
                } else {
                    out.append(String.format("%s\t\t%s %s = null;\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName));
                }
            } catch (NoSuchMethodException e) {
                out.append(String.format("%s\t\t%s %s = null;\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName));
            }

            processObject(sourceElementClass, destinationElementClass, sourceElementClass, ((ParameterizedType) destinationGeneric).getActualTypeArguments()[0], sourcePropertyName, destinationPropertyName, out, depth + 2);

            out.append(String.format("%s\t\t%s.add(%s);\n", t, destinationObjectName, destinationPropertyName));
            out.append(String.format("%s\t}\n", t));
        } else {
            out.append(String.format("%s/* Both source and destination are arrays */\n", t));
            String indexVarName = getUniqueName("index_");
            out.append(String.format("%s\tfor (int %s = 0; %s < %s.length; %s++) {\n", t, indexVarName, indexVarName, sourceObjectName, indexVarName));
            String sourcePropertyName = getUniqueName("sourceCollectionElement_");
            String destinationPropertyName = getUniqueName("destinationCollectionElement_");

            Class sourceElementClass;
            try {
                sourceElementClass = Class.forName(((Class) sourceGeneric).getName().substring(1));
            } catch (ClassNotFoundException e) {
                try {
                    sourceElementClass = Class.forName(((Class) sourceGeneric).getName().substring(1, ((Class) sourceGeneric).getName().length() - 1));
                } catch (Exception e1) {
                    sourceElementClass = getBoxClass(((Class) sourceGeneric).getName().substring(1));
                }
            }
            String sourceElementClassName = ((Class) sourceGeneric).getName().substring(1);
            String sourceTypeSuffix = "";
            while (sourceElementClassName.startsWith("[")) {
                sourceElementClassName = sourceElementClassName.substring(1);
                if (sourceTypeSuffix.length() == 0) {
                    sourceTypeSuffix += "[%s]";
                } else {
                    sourceTypeSuffix += "[]";
                }
            }
            if (sourceElementClassName.length() == 1) {
                sourceElementClassName = getPrimitiveName(sourceElementClassName);
            } else {
                sourceElementClassName = sourceElementClassName.substring(1, sourceElementClassName.length() - 1).replaceAll("\\$", ".");
            }
            sourceElementClassName += sourceTypeSuffix;

            Class destinationElementClass;
            try {
                destinationElementClass = Class.forName(((Class) destinationGeneric).getName().substring(1));
            } catch (ClassNotFoundException e) {
                try {
                    destinationElementClass = Class.forName(((Class) destinationGeneric).getName().substring(2, ((Class) destinationGeneric).getName().length() - 1));
                } catch (Exception e1) {
                    destinationElementClass = getBoxClass(((Class) destinationGeneric).getName().substring(1));
                }
            }

            String destinationElementClassName = ((Class) destinationGeneric).getName().substring(1);
            String destinationTypeSuffix = "";
            while (destinationElementClassName.startsWith("[")) {
                destinationElementClassName = destinationElementClassName.substring(1);
                if (destinationTypeSuffix.length() == 0) {
                    destinationTypeSuffix += "[%s]";
                } else {
                    destinationTypeSuffix += "[]";
                }
            }
            if (destinationElementClassName.length() == 1) {
                destinationElementClassName = getPrimitiveName(destinationElementClassName);
            } else {
                destinationElementClassName = destinationElementClassName.substring(1, destinationElementClassName.length() - 1).replaceAll("\\$", ".");
            }
            destinationElementClassName += destinationTypeSuffix;

            out.append(String.format("%s\t\t%s %s = %s[%s];\n", t, String.format(sourceElementClassName, ""), sourcePropertyName, sourceObjectName, indexVarName));

            if (destinationElementClass.isArray()) {
                String lengthVarName = getUniqueName("arrayLength_");
                out.append(String.format("%s\t\tint %s;\n%s\t\tif (%s == null) {\n%s\t\t\t%s = 0;\n%s\t\t} else {\n%s\t\t\t%s = %s.length;\n%s\t\t}\n", t, lengthVarName, t, sourcePropertyName, t, lengthVarName, t, t, lengthVarName, sourcePropertyName, t));
                out.append(String.format("%s\t\t%s %s = new %s;\n", t, String.format(destinationElementClassName, ""), destinationPropertyName, String.format(destinationElementClassName, lengthVarName)));
            } else if (destinationElementClass.isPrimitive()) {
                out.append(String.format("%s\t\t%s %s = %s;\n", t, destinationElementClass.getName(), destinationPropertyName, getDefaultPrimitiveValue(destinationElementClass.getName())));
            } else {
                try {
                    if (Collection.class.isAssignableFrom(destinationElementClass) || Map.class.isAssignableFrom(destinationElementClass)) {
                        out.append(String.format("%s\t\t%s %s = new %s();\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName, getImpl(destinationElementClass).getConstructor().getName()));
                    } else if (Modifier.isPublic(destinationElementClass.getConstructor().getModifiers())) {
                        out.append(String.format("%s\t\t%s %s = new %s();\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName, destinationElementClass.getConstructor().getName()));
                    } else {
                        out.append(String.format("%s\t\t%s %s = null;\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName));
                    }
                } catch (NoSuchMethodException e) {
                    out.append(String.format("%s\t\t%s %s = null;\n", t, destinationElementClass.getName().replaceAll("\\$", "."), destinationPropertyName));
                }
            }
            processObject(sourceElementClass, destinationElementClass, sourceElementClass, destinationElementClass, sourcePropertyName, destinationPropertyName, out, depth + 2);

            out.append(String.format("%s\t\t%s[%s] = %s;\n", t, destinationObjectName, indexVarName, destinationPropertyName));
            out.append(String.format("%s\t}\n", t));
        }
        out.append(String.format("%s}\n\n", t));
    }

    /**
     * Get boxing class for primitive value.
     *
     * @param destType primitive name
     * @return Class
     */

    private static Class getBoxClass(String destType) {
        if (destType.length() != 1) {
            throw new IllegalArgumentException("getBoxClass cannot be applied to " + destType + " - use only on single-character signatures");
        }
        switch (destType.charAt(0)) {
            case 'Z':
                return Boolean.TYPE;
            case 'B':
                return Byte.TYPE;
            case 'C':
                return Character.TYPE;
            case 'S':
                return Short.TYPE;
            case 'I':
                return Integer.TYPE;
            case 'J':
                return Long.TYPE;
            case 'F':
                return Float.TYPE;
            case 'D':
                return Double.TYPE;
        }
        return null;
    }

    /**
     * Method get default primitive value.
     *
     * @param primitiveName name of primitive
     * @return String default value
     */

    private static String getDefaultPrimitiveValue(String primitiveName) {
        if (primitiveName.equals("boolean")) {
            return "false";
        } else {
            return "0";
        }
    }

    /**
     * Generate primitive mapping.
     *
     * @param sourceClass           source class
     * @param destinationClass      destination class
     * @param sourceObjectName      source object variable name
     * @param destinationObjectName destination object variable name
     * @param out                   output string builder
     * @param depth                 formatting depth
     */

    private static void processPrimitive(final Class sourceClass, final Class destinationClass, final String sourceObjectName, final String destinationObjectName, final StringBuilder out, final int depth) {
        String t = tabs(depth);
        if (destinationClass.isPrimitive() && sourceClass.isPrimitive()) {
            out.append(String.format("%s/* Assign primitive value */\n", t));
            out.append(String.format("%stry {\n%s\t%s = (%s) %s;\n%s} catch (Exception ignored) {}\n\n", t, t, destinationObjectName, destinationClass.getName(), sourceObjectName, t));
        } else if (!destinationClass.isPrimitive() && !sourceClass.isPrimitive()) {
            out.append(String.format("%s/* Assign Number/Boolean/Character value */\n", t));
            out.append(String.format("%sif (%s == null) {\n%s\t%s = null;\n%s} else {\n", t, sourceObjectName, t, destinationObjectName, t));
            String type = destinationClass.getSimpleName().toLowerCase();
            if ("integer".equals(type)) {
                type = "int";
            } else if ("character".equals(type)) {
                type = "char";
            }
            out.append(String.format("%s\t%s = (new %s(%s.%sValue()));\n%s}\n\n", t, destinationObjectName, destinationClass.getName(), sourceObjectName, type, t));
        } else if (destinationClass.isPrimitive()) {
            out.append(String.format("%s/* Assign Number/Boolean/Character value to primitive */\n", t));
            out.append(String.format("%sif (%s != null) {\n", t, sourceObjectName));
            String type = destinationClass.getSimpleName().toLowerCase();
            if ("integer".equals(type)) {
                type = "int";
            } else if ("character".equals(type)) {
                type = "char";
            }
            out.append(String.format("%s\t%s = %s.%sValue();\n%s}\n\n", t, destinationObjectName, sourceObjectName, type, t));
        } else if (sourceClass.isPrimitive()) {
            String type = destinationClass.getSimpleName().toLowerCase();
            if ("integer".equals(type)) {
                type = "int";
            } else if ("character".equals(type)) {
                type = "char";
            }
            out.append(String.format("%s/* Assign primitive value to Number/Boolean/Character */\n", t));
            out.append(String.format("%s%s = new %s((%s) %s);\n\n", t, destinationObjectName, destinationClass.getName(), type, sourceObjectName));
        } else {
            out.append(String.format("\n%s/* Forgot something? TODO check */\n\n", t));
        }
    }

    /**
     * Check if provided method is setter.
     *
     * @param method method to check
     * @return true if method is setter
     */

    private static boolean isSetter(final Method method) {
        return method.getReturnType().getName().equals("void") && method.getName().startsWith("set") && method.getName().length() > 3 && method.getParameterTypes().length == 1;
    }

    /**
     * Get getter method corresponding to provided setter from provided class.
     *
     * @param sourceClass class to find getter in
     * @param setter      setter to find corresponding getter for
     * @return Method or null if there is no matching getter
     */

    private static Method getGetter(final Class sourceClass, final Method setter) {
        final String pattern = setter.getName().substring(3);
        for (Method method : sourceClass.getMethods()) {
//            if (method.getName().endsWith(pattern) && method.getParameterTypes().length == 0 && !method.getReturnType().getName().equals("void")) {
            if (method.getName().matches("^[a-z]{2,5}" + pattern + "$") && method.getParameterTypes().length == 0 && !method.getReturnType().getName().equals("void")) {
                return method;
            }
        }
        return null;
    }

    /**
     * Get primitive name from one-character signature.
     *
     * @param destType signature
     * @return String primitive name
     */

    private static String getPrimitiveName(String destType) {
        switch (destType.charAt(0)) {
            case 'Z':
                return "boolean";
            case 'B':
                return "byte";
            case 'C':
                return "char";
            case 'S':
                return "short";
            case 'I':
                return "int";
            case 'J':
                return "long";
            case 'F':
                return "float";
            case 'D':
                return "double";
        }
        return destType;
    }

    /**
     * Get Collection/Map default implementation.
     *
     * @param clazz interface of implementation to get for
     * @return Class
     */

    private static Class getImpl(Class clazz) {
        try {
            clazz.newInstance();
            return clazz;
        } catch (IllegalAccessException e) {
            throw new IllegalArgumentException("Error while accessing class " + clazz.getName() + " constructor");
        } catch (InstantiationException e) {
            if (java.util.List.class.isAssignableFrom(clazz)) {
                return java.util.ArrayList.class;
            } else if (java.util.Set.class.isAssignableFrom(clazz)) {
                return java.util.HashSet.class;
            } else if (java.util.Queue.class.isAssignableFrom(clazz)) {
                return java.util.LinkedList.class;
            } else if (java.util.concurrent.BlockingQueue.class.isAssignableFrom(clazz)) {
                return java.util.concurrent.LinkedBlockingQueue.class;
            } else if (java.util.Map.class.isAssignableFrom(clazz)) {
                return java.util.HashMap.class;
            } else if (java.util.Collection.class.isAssignableFrom(clazz)) {
                return java.util.ArrayList.class;
            } else {
                throw new IllegalArgumentException("Class " + clazz.getName() + " is an abstract class or interface and default implementation for it cannot be found");
            }
        }
    }
}
